測試是個非常重要的主題,還記得好幾年前,筆者去某大公司面試的時候,其中一題面試題目是:
請說出測試 3A 是哪 3A ?
筆者的表情馬上變成 (A_A)
,因為只有二個 A ,所以後來面試就沒有過了。
相信有了筆者的前車之鑑,大家應該會很想筆記下測試 3A 是哪三個吧,在解答前我們要先來介紹今天的主角 Unit Test。
什麼是 Unit Test 呢?中文又叫單元測試,是測試每一個最小單元(函式)是否能夠正常執行的一種測試。
我們要怎麼測試一個函式是否正確執行呢?
就是使用測試 3A 的原則:
聽起來非常簡單直覺,但很多時候我們的函式是無法(或很難)測試的,比如說:
所有的問題都回歸到一個源頭,我們寫的程式不夠好測。
這時候就需要做些函式的重構或是架構調整才有辦法讓程式碼變的好測試。
所以近年來出現了另一種開發的流程叫做 TDD ,試著要解決這個問題。
TDD stands for Test-Driven-Development,中文有人翻為測試先行,既然函式很難寫測試,那我們可不可以用測試的角度來規劃程式該怎麼寫。另一方面我們也不知道我們寫的測試有沒有幫助,所以 TDD 寫完測試後會先跑一次得到 fail 的結果,才回頭把程式主體寫完,當測試狀態從 fail 變成 success,就是寫完的時候了。
是不是超像在寫 leetcode 的......
我們不在此太多著墨,有興趣的讀者請自行參考:
https://en.wikipedia.org/wiki/Test-driven_development
JUnit 是可以在 Java 層運作的一個 Unit test framework。
通常專案建立的時候都會自動設好了,但還是可以先檢查是否已經有正確的 dependency 宣告:
dependencies {
testImplementation 'junit:junit:4.12'
}
testImplementation
代表這個 dependency 只會在測試的時候需要。
假設我們有個 class 如下:
class AuthManager {
fun validLogin(account: String, password: String): Boolean {
if (account.length < 6) {
return false
} else if (password.length < 8) {
return false
}
return true
}
}
validLogin
函式會需要檢查 account
跟 password
的長度,我們把游標移到 class 上按下 ⌥ + ↩ ,選擇 Create test,Android Studio 會跳出建立測試的畫面如下:
setup/@Before 跟 tearDown/@After 是跑在每個測試項目的一開始跟最後的二個函式。
當有多個測試函式在同個檔案時, setup
跟 tearDown
就會被呼叫多次,如果有些共用的邏輯可以使用這二個函式來處理初始化或是清除資源等。
下方 Member 區塊裡的函式列表有被打勾的話會建立相對應的測試函式,也可以略過自己手動建立。
接下來會問你要放在哪個目錄下,還記得我們在 Hello world 之專案結構 有提到,androidTest/
是跟 Android sdk 有關的測試,而 test/
是跟 platform 無關的測試。
我們的 AuthManager
跟 platform 無關,所以選擇 test/
目錄,接下來就會看到自動建立的 test class 內容如下:
class AuthManagerTest {
@Before
fun setUp() {
}
@After
fun tearDown() {
}
@Test
fun validLogin() {
}
}
@Test
是 JUnit 提供的 annotation,代表一個實際的測試案例,我們先寫一個會失敗的例子如下:
@Test
fun validLogin() {
val authManager = AuthManager()
val result = authManager.validLogin("123456", "")
Assert.assertEquals(true, result)
}
我們預期密碼為空值的時候登入也會成功,所以應該要是個失敗的測試。
IDE 在函式的前面會有個綠色小箭頭可以點來單獨跑一個測試,也可以在檔案上按右鍵選 Run 'AuthManagerTest',或是在你的 module 上按右鍵選擇 Run 'All Tests'
會得到以下的結果
java.lang.AssertionError:
Expected :true
Actual :false
<Click to see difference>
我們把 test 修改為正確的例子如下:
@Test
fun validLogin() {
val authManager = AuthManager()
val result = authManager.validLogin("123456", "12345678")
Assert.assertEquals(true, result)
}
再跑一次,現在就會正常了!
是不是很有趣呢?
但其實還沒完,validLogin
有二個參數而且都是 string,這個排列組合有超多可能的,我們怎麼知道我們要寫多少測試呢?
通常我們無法測試所有的條件,而且事實我們也不需要如此,就像二元一次方程式只要二個點就可以決定一個線,只要了解商業邏輯,把正常跟會導致錯誤的條件都涵蓋的話,理論上就已經足夠了。
另一個技巧是降維度,假設 string 有 n 種可能,我們的 validLogin
將會有 n * n 種可能,如果我們將 validLogin
拆成 validAccount
跟 validPassword
二個,就會變成只有 2 * n 種可能,而且拆出來的函式也能更清楚表達,是不是更美好呢?
以上就是今天的全部內容了,希望大家都能掌握 3A 的原則!!
歡迎參考 Android 十全大補的測試三部曲的其他文章:
參考資料:
https://developer.android.com/training/testing/fundamentals
https://developer.android.com/studio/test
https://en.wikipedia.org/wiki/Unit_testing
Android 十全大補已經正式出書上架囉!
有興趣的讀者歡迎參考:
https://www.tenlong.com.tw/products/9789864345786